/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.rest.data.service;

import static org.unidata.mdm.meta.type.search.RelationHeaderField.FIELD_DIRECTION_FROM;
import static org.unidata.mdm.meta.type.search.RelationHeaderField.FIELD_FROM;
import static org.unidata.mdm.meta.type.search.RelationHeaderField.FIELD_FROM_ETALON_ID;
import static org.unidata.mdm.meta.type.search.RelationHeaderField.FIELD_RELATION_NAME;
import static org.unidata.mdm.meta.type.search.RelationHeaderField.FIELD_TO;
import static org.unidata.mdm.meta.type.search.RelationHeaderField.FIELD_TO_ETALON_ID;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.DataRecord;
import org.unidata.mdm.core.type.data.OperationType;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.type.timeline.TimeInterval;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.data.context.DeleteRelationRequestContext;
import org.unidata.mdm.data.context.GetRelationRequestContext;
import org.unidata.mdm.data.context.GetRelationTimelineRequestContext;
import org.unidata.mdm.data.context.GetRelationsDigestRequestContext;
import org.unidata.mdm.data.context.GetRelationsRequestContext;
import org.unidata.mdm.data.context.GetRelationsTimelineRequestContext;
import org.unidata.mdm.data.context.UpsertRelationRequestContext;
import org.unidata.mdm.data.context.UpsertRelationsRequestContext;
import org.unidata.mdm.data.dto.DeleteRelationDTO;
import org.unidata.mdm.data.dto.GetRelationDTO;
import org.unidata.mdm.data.dto.RelationDigestDTO;
import org.unidata.mdm.data.dto.RelationStateDTO;
import org.unidata.mdm.data.dto.UpsertRelationDTO;
import org.unidata.mdm.data.dto.UpsertRelationsDTO;
import org.unidata.mdm.data.service.DataRelationsService;
import org.unidata.mdm.data.service.impl.CommonRelationsComponent;
import org.unidata.mdm.data.type.data.EtalonRelation;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.data.RelationType;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.type.RelativeDirection;
import org.unidata.mdm.meta.type.search.EntityIndexType;
import org.unidata.mdm.meta.type.search.RecordHeaderField;
import org.unidata.mdm.meta.type.search.RelationHeaderField;
import org.unidata.mdm.rest.core.converter.RoleRoConverter;
import org.unidata.mdm.rest.data.converter.ErrorInfoToRestErrorInfoConverter;
import org.unidata.mdm.rest.data.converter.IntegralRecordEtalonConverter;
import org.unidata.mdm.rest.data.converter.RelationToEtalonConverter;
import org.unidata.mdm.rest.data.converter.TimelineToTimelineROConverter;
import org.unidata.mdm.rest.data.ro.BaseRelationRO;
import org.unidata.mdm.rest.data.ro.EtalonIntegralRecordRO;
import org.unidata.mdm.rest.data.ro.EtalonRecordRO;
import org.unidata.mdm.rest.data.ro.EtalonRelationToRO;
import org.unidata.mdm.rest.data.ro.RecordTimelineRO;
import org.unidata.mdm.rest.data.ro.RelationDigestRO;
import org.unidata.mdm.rest.data.ro.RelationSearcResultRO;
import org.unidata.mdm.rest.data.ro.RelationSearchCellRO;
import org.unidata.mdm.rest.data.ro.RelationSearchRO;
import org.unidata.mdm.rest.data.ro.RelationViewRO;
import org.unidata.mdm.rest.data.ro.TimelineRO;
import org.unidata.mdm.rest.data.util.RestUtils;
import org.unidata.mdm.rest.search.converter.SearchResultToRestSearchResultConverter;
import org.unidata.mdm.rest.search.ro.SearchResultHitFieldRO;
import org.unidata.mdm.rest.search.ro.SearchResultHitRO;
import org.unidata.mdm.rest.search.ro.SearchResultRO;
import org.unidata.mdm.rest.search.type.result.SearchResultProcessingElements;
import org.unidata.mdm.rest.system.ro.ErrorInfo;
import org.unidata.mdm.rest.system.ro.ErrorResponse;
import org.unidata.mdm.rest.system.ro.RestResponse;
import org.unidata.mdm.rest.system.service.AbstractRestService;
import org.unidata.mdm.rest.system.util.RestConstants;
import org.unidata.mdm.search.context.ComplexSearchRequestContext;
import org.unidata.mdm.search.context.SearchRequestContext;
import org.unidata.mdm.search.dto.ComplexSearchResultDTO;
import org.unidata.mdm.search.dto.SearchResultDTO;
import org.unidata.mdm.search.dto.SearchResultHitDTO;
import org.unidata.mdm.search.service.SearchService;
import org.unidata.mdm.search.type.form.FieldsGroup;
import org.unidata.mdm.search.type.form.FormField;
import org.unidata.mdm.search.type.query.SearchQuery;
import org.unidata.mdm.search.type.sort.SortField;
import org.unidata.mdm.search.util.SearchUtils;
import org.unidata.mdm.system.type.runtime.MeasurementContextName;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;
import org.unidata.mdm.system.util.ConvertUtils;
import org.unidata.mdm.system.util.IdUtils;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * @author Mikhail Mikhailov
 */
@Path(DataRelationsRestService.PATH_PARAM_RELATIONS )
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class DataRelationsRestService extends AbstractRestService {

    /**
     * Relations.
     */
    public static final String PATH_PARAM_RELATIONS = "relations";
    /**
     * Relations search.
     */
    public static final String PATH_PARAM_SEARCH = "search";
    /**
     * Relations digest.
     */
    public static final String PATH_PARAM_DIGEST = "digest";
    /**
     * Relation.
     */
    public static final String PATH_PARAM_RELATION = "relation";
    /**
     * Relation to specific.
     */
    public static final String PATH_PARAM_RELTO = "relto";
    /**
     * Integral specific.
     */
    public static final String PATH_PARAM_INTEGRAL = "integral";
    /**
     * Data relation facade.
     */
    @Autowired
    private DataRelationsService dataRelationsService;
    /**
     * Meta model service.
     */
    @Autowired
    private MetaModelService metaModelService;
    /**
     * Search service.
     */
    @Autowired
    private SearchService searchService;
    /**
     * Modify search result for ui presentation
     */
    @Autowired
    private SearchResultHitModifier searchResultHitModifier;

    @Autowired
    private CommonRelationsComponent commonRelationsComponent;
    /**
     * Constructor.
     */
    public DataRelationsRestService() {
        super();
    }

    /**
     * Gets etalon relation data object by relation etalon ID.
     * @param etalonId relation's etalon ID
     * @param name rel name
     * @param dateAsString date
     * @param includeInactiveAsString include inactive
     * @param includeDraftsAsString include drafts
     * @return response array of time line objects
     */
    @GET
    @Path("/" + PATH_PARAM_RELATION
            + "/" + RestConstants.PATH_PARAM_TIMELINE
            + "/{" + RestConstants.DATA_PARAM_ID + "}{p:/?}{"
            + RestConstants.DATA_PARAM_DATE
            + ": " + RestConstants.DEFAULT_TIMESTAMP_PATTERN + "}")
    @Operation(
        description = "Запросить связи эталона на определенную дату либо на сейчас, если дата не задана.",
        method = "GET",
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getRelationTimelineByRelationEtalonIdAndDate(
            @Parameter(description = "ID эталонной записи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            @Parameter(description = "Имя связи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_NAME) String name,
            @Parameter(description = "Дата для получения периода.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_DATE) String dateAsString,
            @Parameter(description = "Включать неактивные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_INACTIVE) String includeInactiveAsString) {

        Date asOf = ConvertUtils.string2Date(dateAsString);

        Timeline<OriginRelation> timeline = commonRelationsComponent.loadTimeline(GetRelationTimelineRequestContext.builder()
                .relationEtalonKey(etalonId)
                .forDate(asOf)
                .fetchData(false)
                .build());

        return Response
                .ok(new RestResponse<>(TimelineToTimelineROConverter.convert(timeline)))
                .build();
    }

    /**
     * Gets etalon relation objects along with their time lines by record etalon ID.
     * @param etalonId record's etalon ID
     * @param timestamps records validity period boundary
     * @return response array of time line objects
     */
    @GET
    @Path("/" + PATH_PARAM_RELATION
            + "/" + RestConstants.PATH_PARAM_TIMELINE
            + "/{" + RestConstants.DATA_PARAM_ID + "}/{" + RestConstants.DATA_PARAM_TIMESTAMPS + ":.*}")
    @Operation(
        description = "Запросить связи эталона на определенную дату либо на сейчас, если дата не задана.",
        method = "GET",
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getRelationTimelineByRelationEtalonIdAndBoundary(
            @Parameter(description = "ID эталонной записи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            @Parameter(description = "Значения границ интервалов. Всегда 2 элемента", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_TIMESTAMPS) List<PathSegment> timestamps,
            @Parameter(description = "Включать неактивные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_INACTIVE) String includeInactiveAsString,
            @Parameter(description = "Включать неподтвержденные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_DRAFTS) String includeDraftsAsString) {

        Date validFrom = RestUtils.extractStart(timestamps);
        Date validTo = RestUtils.extractEnd(timestamps);

        Timeline<OriginRelation> timeline = commonRelationsComponent.loadTimeline(GetRelationTimelineRequestContext.builder()
                .relationEtalonKey(etalonId)
                .forDatesFrame(Pair.<Date, Date>of(validFrom, validTo))
                .fetchData(false)
                .build());

        return Response.ok(new RestResponse<>(TimelineToTimelineROConverter.convert(timeline))).build();
    }

    /**
     * Gets etalon relation data object by relation etalon ID.
     * @param etalonId relation's etalon ID
     * @param dateAsString
     * @return response array of time line objects
     */
    @GET
    @Path("/" + RestConstants.PATH_PARAM_TIMELINE
            + "/{" + RestConstants.DATA_PARAM_ID + "}/{" + RestConstants.DATA_PARAM_NAME + "}{p:/?}{"
            + RestConstants.DATA_PARAM_DATE + ": " + RestConstants.DEFAULT_TIMESTAMP_PATTERN + "}")
    @Operation(
        description = "Запросить связи эталона на определенную дату либо на сейчас, если дата не задана.",
        method = "GET",
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getRelationsTimelineByRecordEtalonIdAndDate(
            @Parameter(description = "ID эталонной записи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            @Parameter(description = "Имя связи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_NAME) String name,
            @Parameter(description = "Дата для получения периода.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_DATE) String dateAsString,
            @Parameter(description = "Включать неактивные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_INACTIVE) String includeInactiveAsString) {

        Date asOf = ConvertUtils.string2Date(dateAsString);

        Map<String, List<Timeline<OriginRelation>>> timelines = commonRelationsComponent.loadTimelines(GetRelationsTimelineRequestContext.builder()
                .etalonKey(etalonId)
                .relationNames(name)
                .forDate(asOf)
                .fetchData(false)
                .reduceReferences(true)
                .build());

        List<TimelineRO> converted = new ArrayList<>();
        TimelineToTimelineROConverter.convert(timelines.values().stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toList()), converted);

        return Response.ok(new RestResponse<>(timelines)).build();
    }

    /**
     * Gets etalon relation objects along with their time lines by record etalon ID.
     * @param etalonId record's etalon ID
     * @param name name of the relation
     * @param timestamps records validity period boundary
     * @return response array of time line objects
     */
    @GET
    @Path("/" + RestConstants.PATH_PARAM_TIMELINE + "/{"
            + RestConstants.DATA_PARAM_ID + "}/{"
            + RestConstants.DATA_PARAM_NAME + "}/{"
            + RestConstants.DATA_PARAM_TIMESTAMPS + ":.*}")
    @Operation(
        description = "Запросить связи эталона на определенную дату либо на сейчас, если дата не задана.",
        method = "GET",
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getRelationsTimelineByRecordEtalonIdAndBoundary(
            @Parameter(description = "ID эталонной записи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            @Parameter(description = "Имя связи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_NAME) String name,
            @Parameter(description = "Значения границ интервалов. Всегда 2 элемента", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_TIMESTAMPS) List<PathSegment> timestamps,
            @Parameter(description = "Включать неактивные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_INACTIVE) String includeInactiveAsString) {

        Date validFrom = RestUtils.extractStart(timestamps);
        Date validTo = RestUtils.extractEnd(timestamps);

        Map<String, List<Timeline<OriginRelation>>> timelines = commonRelationsComponent.loadTimelines(GetRelationsTimelineRequestContext.builder()
                .etalonKey(etalonId)
                .relationNames(name)
                .forDatesFrame(Pair.<Date, Date>of(validFrom, validTo))
                .fetchData(false)
                .reduceReferences(true)
                .build());

        List<TimelineRO> converted = new ArrayList<>();
        TimelineToTimelineROConverter.convert(timelines.values().stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toList()), converted);

        return Response.ok(new RestResponse<>(timelines)).build();
    }

    /**
     * Gets etalon relation objects along with their time lines by record etalon ID.
     * @param etalonId record's etalon ID
     * @param name name of the relation
     * @param timestamps records validity period boundary
     * @return response array of time line objects
     */
    @GET
    @Path("/" + RestConstants.PATH_PARAM_RELATION_BULK + "/{"
            + RestConstants.DATA_PARAM_ID + "}/{"
            + RestConstants.DATA_PARAM_NAME + "}/{"
            + RestConstants.DATA_PARAM_TIMESTAMPS + ":.*}")
    @Operation(
        description = "Запросить связи эталона на определенную дату либо на сейчас, если дата не задана.",
        method = "GET",
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getRelationsByRecordEtalonIdAndBoundary(
            @Parameter(description = "ID эталонной записи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            @Parameter(description = "Имя связи.", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_NAME) String name,
            @Parameter(description = "Значения границ интервалов. Всегда 2 элемента", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_TIMESTAMPS) List<PathSegment> timestamps,
            @Parameter(description = "Включать неактивные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_INACTIVE) String includeInactiveAsString,
            @Parameter(description = "Включать неподтвержденные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_DRAFTS) String includeDraftsAsString,
            @Parameter(description = "Список возвращаемых полей", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_RETURN_FIELDS) List<String> returnFields) {

        Date validFrom = RestUtils.extractStart(timestamps);
        Date validTo = RestUtils.extractEnd(timestamps);
        boolean includeDrafts = BooleanUtils.toBoolean(includeDraftsAsString);
        List<BaseRelationRO> relations = new ArrayList<>();
        RelationElement def = metaModelService.instance(Descriptors.DATA).getRelation(name);
        Date now = new Date();

        Map<String, List<Triple<String, Date, Date>>> displayNames = searchResultHitModifier.getDisplayNamesForRelationTo(name, etalonId, null, validFrom, validTo);
        if (includeDrafts) {

            GetRelationsRequestContext tlCtx = GetRelationsRequestContext.builder()
                    .etalonKey(etalonId)
                    .relationNames(name)
                    .forDatesFrame(new ImmutablePair<>(validFrom, validTo))
                    .includeDrafts(includeDrafts)
                    .reduceReferences(true)
                    .fetchTimelineData(true)
                    .build();

            List<Timeline<OriginRelation>> returned = dataRelationsService.loadTimelines(tlCtx);
            for (Iterator<Timeline<OriginRelation>> tl = returned.iterator(); tl.hasNext(); ) {
                boolean hasActivePeriods = tl.next().isActive();
                if (!hasActivePeriods) {
                    tl.remove();
                }
            }

            if(!returned.isEmpty()){
                for(Timeline<OriginRelation> timeline : returned){

                    for(TimeInterval<OriginRelation> timeInterval : timeline){
                        if (!timeInterval.isActive()) {
                            continue;
                        }
                        BaseRelationRO converted = null;
                        if (timeInterval.getCalculationResult() instanceof EtalonRelation) {
                            EtalonRelation etalonRelation = (EtalonRelation) timeInterval.getCalculationResult();
                            if (etalonRelation.getInfoSection().getRelationType() == RelationType.CONTAINS) {
                                converted = IntegralRecordEtalonConverter.to(etalonRelation);
                                if (converted != null) {
                                    ((EtalonIntegralRecordRO) converted).setEtalonId(etalonRelation.getInfoSection().getRelationEtalonKey());
                                }
                            } else if (etalonRelation.getInfoSection().getRelationType() == RelationType.REFERENCES
                                    || etalonRelation.getInfoSection().getRelationType() == RelationType.MANY_TO_MANY) {

                                converted = RelationToEtalonConverter.to(etalonRelation);
                                if (converted != null) {
                                    EtalonRelationToRO etalonRelationToRO = (EtalonRelationToRO) converted;
                                    etalonRelationToRO.setEtalonId(timeline.getKeys().getEtalonKey().getId());
                                    etalonRelationToRO.setEtalonDisplayNameTo(searchResultHitModifier.extractDisplayName(displayNames,
                                            etalonRelationToRO.getEtalonIdTo(),
                                            ConvertUtils.localDateTime2Date(etalonRelationToRO.getValidFrom()),
                                            ConvertUtils.localDateTime2Date(etalonRelationToRO.getValidTo()),
                                            now));
                                }
                            }
                            relations.add(converted);
                        }
                    }
                }
            }
        } else {

            if (def.isContainment()) {

                if (CollectionUtils.isEmpty(returnFields)){
                    returnFields = new ArrayList<>(def.getRight().getMainDisplayableAttributes().keySet());
                }

                return Response.ok(new RestResponse<>(getRelationsToSideByTimelineAndFromEtalonId(etalonId,
                        validFrom,
                        validTo,
                        def,
                        returnFields)))
                        .build();
            } else {

                if (CollectionUtils.isEmpty(returnFields)) {

                    returnFields = def.getAttributes().values()
                            .stream()
                            .map(AttributeElement::getName)
                            .collect(Collectors.toList());
                }

                SearchResultDTO relationsSearchResult = getRelationsByTimelineAndFromEtalonId(etalonId, validFrom, validTo, def, returnFields);
                relations.addAll(RelationToEtalonConverter.to(relationsSearchResult, def));
                relations.forEach(relation -> {
                    EtalonRelationToRO relationTO = (EtalonRelationToRO) relation;
                    relationTO.setEtalonDisplayNameTo(searchResultHitModifier.extractDisplayName(displayNames,
                            relationTO.getEtalonIdTo(),
                            ConvertUtils.localDateTime2Date(relationTO.getValidFrom()),
                            ConvertUtils.localDateTime2Date(relationTO.getValidTo()),
                            now));
                });
            }
        }
        relations.sort(Comparator.comparing(BaseRelationRO::getCreateDate));
        return Response.ok(new RestResponse<>(relations)).build();
    }

    @POST
    @Path("/" + PATH_PARAM_SEARCH)
    @Operation(
        description = "Запросить табличную информацию по связям для набора периодов записей.",
        method = "POST",
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = RelationSearchRO.class)), description = "Поисковый запрос"),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getRelationInfo(RelationSearchRO request) {
        Map<String, Map<String, List<RelationSearchCellRO>>> table = new HashMap<>();
        // build data for all cell in table relName x record period
        request.getRelationNames().forEach(relName -> {
            Map<String, List<RelationSearchCellRO>> tableColumn = new HashMap<>();
            request.getFromEtalonIds().forEach(recordTimelineRO -> {
                RelationSearchCellRO cellRO = new RelationSearchCellRO();
                cellRO.setRelName(relName);
                cellRO.setEtalonId(recordTimelineRO.getEtalonId());
                cellRO.setValidFrom(ConvertUtils.localDateTime2Date(recordTimelineRO.getValidFrom()));
                cellRO.setValidTo(ConvertUtils.localDateTime2Date(recordTimelineRO.getValidTo()));
                tableColumn.computeIfAbsent(recordTimelineRO.getEtalonId(), s -> new ArrayList<>()).add(cellRO);
            });
            table.put(relName, tableColumn);
        });

        FieldsGroup restrictions = FieldsGroup.and();
        FieldsGroup idsFilter = FieldsGroup.or();
        Date now = new Date();
        for (RecordTimelineRO timeline : request.getFromEtalonIds()) {
            idsFilter.add(FieldsGroup.and(
                    FormField.exact(FIELD_FROM_ETALON_ID, timeline.getEtalonId()),
                    FormField.range(FIELD_FROM, null, timeline.getValidTo()),
                    FormField.range(FIELD_TO, timeline.getValidFrom(), null),
                    FormField.exact(FIELD_DIRECTION_FROM, true)
            ));
        }
        restrictions.add(idsFilter);
        restrictions.add(FormField.exact(FIELD_RELATION_NAME, request.getRelationNames()));

        SearchRequestContext relCtx = SearchRequestContext.builder(EntityIndexType.RELATION, request.getEntity(), SecurityUtils.getCurrentUserStorageId())
                .query(SearchQuery.formQuery(restrictions))
                .returnFields(
                        FIELD_FROM.getName(),
                        FIELD_TO.getName(),
                        FIELD_RELATION_NAME.getName(),
                        FIELD_FROM_ETALON_ID.getName(),
                        FIELD_TO_ETALON_ID.getName())
                .sorting(Collections.singletonList(SortField.of(FIELD_FROM, SortField.SortOrder.ASC)))
                .page(0)
                .count(SearchRequestContext.MAX_PAGE_SIZE)
                .build();

        SearchResultDTO relationResult = searchService.search(relCtx);
        // fill information about relations in cell
        for (SearchResultHitDTO hit : relationResult.getHits()) {
            String relName = hit.getFieldFirstValue(FIELD_RELATION_NAME.getName());
            String etalonIdFrom = hit.getFieldFirstValue(FIELD_FROM_ETALON_ID.getName());
            Date relationFrom = SearchUtils.parse(hit.getFieldFirstValue(FIELD_FROM.getName()));
            Date relationTo = SearchUtils.parse(hit.getFieldFirstValue(FIELD_TO.getName()));
            // Always not empty list
            List<RelationSearchCellRO> cellRoList = table.get(relName).get(etalonIdFrom);
            for (RelationSearchCellRO cellRO : cellRoList) {
                if ((cellRO.getValidFrom() == null || relationTo == null || !cellRO.getValidFrom().after(relationTo))
                        && (cellRO.getValidTo() == null || relationFrom == null || !cellRO.getValidTo().before(relationFrom))) {

                    cellRO.setTotal(cellRO.getTotal() + 1);
                    if (cellRO.getData() == null) {
                        cellRO.setData(new ArrayList<>());
                    }
                    if (cellRO.getData().size() < request.getMaxCount()) {
                        RelationViewRO view = new RelationViewRO();
                        view.setRelName(relName);
                        view.setEtalonIdFrom(etalonIdFrom);
                        view.setEtalonIdTo(hit.getFieldFirstValue(FIELD_TO_ETALON_ID.getName()));
                        view.setValidFrom(SearchUtils.parse(hit.getFieldFirstValue(FIELD_FROM.getName())));
                        view.setValidTo(SearchUtils.parse(hit.getFieldFirstValue(FIELD_TO.getName())));
                        cellRO.getData().add(view);
                    }
                }
            }
        }
        // remove empty cells
        table.values().stream()
                .flatMap(t -> t.values().stream())
                .forEach(t -> t.removeIf(cellRo -> CollectionUtils.isEmpty(cellRo.getData())));

        // calculate display names
        for (Map.Entry<String, Map<String, List<RelationSearchCellRO>>> entry : table.entrySet()) {

            List<Triple<String, Date, Date>> dataIds = new ArrayList<>();
            entry.getValue().values().stream().flatMap(Collection::stream).forEach(row -> {
                if (row.getData() != null) {
                    for (RelationViewRO viewRO : row.getData()) {
                        dataIds.add(Triple.of(viewRO.getEtalonIdTo(), viewRO.getValidFrom(), viewRO.getValidTo()));
                    }
                }
            });

            Map<String, List<Triple<String, Date, Date>>> displayNames = searchResultHitModifier.getDisplayNamesForRelationTo(entry.getKey(), dataIds);

            entry.getValue().values().stream().flatMap(Collection::stream).forEach(row -> {
                for (RelationViewRO viewRO : row.getData()) {
                    viewRO.setEtalonDisplayNameTo(searchResultHitModifier.extractDisplayName(displayNames, viewRO.getEtalonIdTo(), viewRO.getValidFrom(), viewRO.getValidTo(), now));
                }
            });
        }

        RelationSearcResultRO result = new RelationSearcResultRO();
        result.setMaxCount(request.getMaxCount());
        result.setData(table.values().stream()
                .flatMap(s -> s.values().stream())
                .flatMap(Collection::stream)
                .collect(Collectors.toList()));
        return Response.ok(new RestResponse<>(result)).build();
    }


    @POST
    @Path("/" + PATH_PARAM_DIGEST)
    @Operation(
        description = "Запросить короткую информацию по связям эталона на определенную дату либо на сейчас, если дата не задана.",
        method = "POST",
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = RelationDigestRO.class)), description = "Поисковый запрос"),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getRelationDigest(RelationDigestRO request) {

        RelationElement def = metaModelService.instance(Descriptors.DATA).getRelation(request.getRelName());
        if (def == null) {
            return emptySearchResult();
        }

        GetRelationsDigestRequestContext ctx = GetRelationsDigestRequestContext.builder()
                .count(request.getCount())
                .totalCount(request.isTotalCount())
                .from(request.getFrom())
                .to(request.getTo())
                .direction(request.getDirection() == null ? RelativeDirection.FROM : request.getDirection())
                .etalonId(request.getEtalonId())
                .fields(request.getFields())
                .page(request.getPage() > 0 ? request.getPage() - 1 : 0)
                .relName(request.getRelName())
                .build();

        RelationDigestDTO digest = dataRelationsService.loadRelatedEtalonIdsForDigest(ctx);
        if (digest == null || digest.getTotalCount() == 0
                || (digest.getEtalonIds() == null || digest.getEtalonIds().isEmpty())) {
            return emptySearchResult();
        }

        String entityName = request.getDirection() == RelativeDirection.FROM ? def.getLeft().getName() : def.getRight().getName();
        SearchRequestContext sCtx = SearchRequestContext.builder(EntityIndexType.RECORD, entityName, SecurityUtils.getCurrentUserStorageId())
                .totalCount(ctx.isTotalCount())
                .query(SearchQuery.formQuery(
                    FieldsGroup.and(FormField.exact(RecordHeaderField.FIELD_ETALON_ID, digest.getEtalonIds()))))
                .returnFields(request.getFields())
                .count(request.getCount())
                .page(0)
                .build();

        SearchResultDTO result = searchService.search(sCtx);
        searchResultHitModifier.modifySearchResult(result);
        SearchResultRO ro = SearchResultToRestSearchResultConverter.convert(result);

        // Post process for records, which have no valid periods for right now
        List<String> allIds = new ArrayList<>(digest.getEtalonIds());
        for (SearchResultHitDTO hit : result.getHits()) {
            allIds.remove(hit.getId());
        }

        // Add remaining IDs
        for (String id : allIds) {
            SearchResultHitRO emptyHit = new SearchResultHitRO(id);
            emptyHit.getPreview().add(new SearchResultHitFieldRO(RecordHeaderField.FIELD_ETALON_ID.getName(), id, Collections.singletonList(id)));
            ro.getHits().add(emptyHit);
        }

        ro.setTotalCount(digest.getTotalCount());
        ro.setHasRecords(digest.getTotalCount() != 0);

        return ok(ro);
    }

    private Response emptySearchResult() {
        SearchResultRO ro = new SearchResultRO();
        ro.setSuccess(true);
        ro.setTotalCount(0);
        ro.setHasRecords(false);
        return ok(ro);
    }

    /**
     * Get relation"s content (either containment or relation to).
     * @param relationEtalonId
     * @param dateAsString
     * @return
     * @throws ParseException
     */
    @GET
    @Path("/" + PATH_PARAM_RELATION
            + "/{" + RestConstants.DATA_PARAM_ID + "}"
            + "{p:/?}{" + RestConstants.DATA_PARAM_DATE + ": " + RestConstants.DEFAULT_TIMESTAMP_PATTERN + "}")
    @Operation(
        description = "Запросить состояние эталона связи на определенную дату либо на сейчас, если дата не задана.",
        method = HttpMethod.GET,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getRelationByEtalonId(
            @Parameter(description = "Эталон ID связи", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String relationEtalonId,
            @Parameter(description = "Эталон ID связи", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_DATE) String dateAsString,
            @Parameter(description = "Включать неактивные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_INACTIVE) String includeInactiveAsString,
            @Parameter(description = "Включать неподтвержденные версии в эталон или нет (true/false)", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_INCLUDE_DRAFTS) String includeDraftsAsString,
            @Parameter(description = "Включить версии совместные с указанным operationId", in = ParameterIn.QUERY) @QueryParam(RestConstants.DATA_PARAM_OPERATION_ID) String operationId) {

        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_RELATIONS_GET);
        MeasurementPoint.start();
        try {

            Date asOf = ConvertUtils.string2Date(dateAsString);

            GetRelationRequestContext ctx = GetRelationRequestContext.builder()
                    .relationEtalonKey(relationEtalonId)
                    .forDate(asOf)
                    .forOperationId(operationId)
                    .build();

            BaseRelationRO converted = null;
            GetRelationDTO result = dataRelationsService.getRelation(ctx);
            if (Objects.nonNull(result) && Objects.nonNull(result.getRelationKeys())) {

                RelationKeys keys = result.getRelationKeys();
                if (keys.getRelationType() == RelationType.CONTAINS) {
                    converted = IntegralRecordEtalonConverter.to(result.getEtalon());
                    if (converted != null) {
                        ((EtalonIntegralRecordRO) converted).setEtalonId(result.getRelationKeys().getEtalonKey().getId());
                        EtalonRecordRO etalonRecord = ((EtalonIntegralRecordRO) converted).getEtalonRecord();
                        if (etalonRecord != null) {
                            // TODO @Modules
//                            List<DataQualityError> dataQualityErrors = dataRecordsService
//                                    .getDQErrors(etalonRecord.getEtalonId(), etalonRecord.getEntityName(), asOf);
//                            DataRecordEtalonConverter.copyDQErrors(dataQualityErrors, etalonRecord.getDqErrors());
//                            etalonRecord.setWorkflowState(WorkflowTaskConverter.to(result.getTasks()));
                            etalonRecord.setRights(RoleRoConverter.convertResourceSpecificRights(result.getRights()));
                        }
                    }
                } else if (keys.getRelationType() == RelationType.REFERENCES
                        || keys.getRelationType() == RelationType.MANY_TO_MANY) {
                    converted = RelationToEtalonConverter.to(result.getEtalon());
                    if (converted != null) {
                        ((EtalonRelationToRO) converted).setEtalonId(result.getRelationKeys().getEtalonKey().getId());
                    }
                }
            }

            return Response.ok(new RestResponse<>(converted)).build();

        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * Upserts etalon relation to relation.
     * @param etalonId the
     * @param ro
     * @return
     * @throws Exception
     */
    @POST
    @Path("/" + PATH_PARAM_RELATION
            + "/" + DataRelationsRestService.PATH_PARAM_RELTO + "/{" + RestConstants.DATA_PARAM_ID + "}")
    @Operation(
        description = "Вставить список связей к текущему объекту.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = EtalonRelationToRO.class)), description = "Запрос на вставку"),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response upsertRelationToRelation(
            @Parameter(description = "ID эталона", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            EtalonRelationToRO ro){

        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_RELATIONS_TO_UPSERT);
        MeasurementPoint.start();
        try {

            DataRecord converted = RelationToEtalonConverter.from(ro);
            Date validFrom = ConvertUtils.localDateTime2Date(ro.getValidFrom());
            Date validTo = ConvertUtils.localDateTime2Date(ro.getValidTo());

            UpsertRelationsRequestContext ctx = UpsertRelationsRequestContext.builder()
                    .etalonKey(etalonId)
                    .relationsFrom(Collections.singletonMap(ro.getRelName(), Collections.singletonList(
                            UpsertRelationRequestContext.builder()
                                    .etalonKey(ro.getEtalonIdTo())
                                    .record(converted)
                                    .relationName(ro.getRelName())
                                    .validFrom(validFrom)
                                    .validTo(validTo)
                                    .sourceSystem(metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement().getName())
                                    .build())))
                    .build();

            UpsertRelationsDTO result = dataRelationsService.upsertRelations(ctx);
            EtalonRelationToRO updated = null;

            // Single record i expected
            Iterator<Entry<RelationStateDTO, List<UpsertRelationDTO>>> i
                    = result != null && result.getRelations() != null
                    ? result.getRelations().entrySet().iterator()
                    : null;

            List<ErrorInfo> errors = null;
            while (i != null && i.hasNext()) {
                List<UpsertRelationDTO> relations = i.next().getValue();

                UpsertRelationDTO dto = relations != null && !relations.isEmpty() ? relations.get(0) : null;
                EtalonRelation relation = dto != null ? dto.getEtalon() : null;

                updated = RelationToEtalonConverter.to(relation);

                if(dto != null && CollectionUtils.isNotEmpty(dto.getErrors())){
                    errors = dto.getErrors()
                            .stream()
                            .map(ErrorInfoToRestErrorInfoConverter::convert)
                            .collect(Collectors.toList());
                }

                if (updated != null) {
                    updated.setEtalonId(dto == null ? null : dto.getRelationKeys().getEtalonKey().getId());
                }

                break;
            }


            // populate to side display name
            Map<String, List<Triple<String, Date, Date>>> displayNames = searchResultHitModifier.getDisplayNamesForRelationTo(updated.getRelName(), etalonId, updated.getEtalonId(),
                     null, null);
            updated.setEtalonDisplayNameTo(searchResultHitModifier.extractDisplayName(displayNames,
                    updated.getEtalonIdTo(),
                    ConvertUtils.localDateTime2Date(updated.getValidFrom()),
                    ConvertUtils.localDateTime2Date(updated.getValidTo()),
                    new Date()));
            RestResponse response = new RestResponse<>(updated);
            response.setErrors(errors);

            return Response.ok(response).build();
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * Upserts etalon relation to relation.
     * @param etalonId the
     * @param ro
     * @return
     * @throws Exception
     */
    @POST
    @Path("/" + PATH_PARAM_RELATION
            + "/" + DataRelationsRestService.PATH_PARAM_INTEGRAL + "/{" + RestConstants.DATA_PARAM_ID + "}")
    @Operation(
        description = "Вставить список связей к текущему объекту.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = EtalonIntegralRecordRO.class)), description = "Запрос на вставку"),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response upsertIntegralRelation(
            @Parameter(description = "ID эталона", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            EtalonIntegralRecordRO ro) {

        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_RELATIONS_INTEGRAL_UPSERT);
        MeasurementPoint.start();
        try {

            DataRecord converted = IntegralRecordEtalonConverter.from(ro);
            // TODO @Modules
            //            ro.getEtalonRecord().setDqErrors(new ArrayList<>());

            String toEtalonId = ro.getEtalonRecord() != null ? ro.getEtalonRecord().getEtalonId() : null;
            String toSsourceSystem = toEtalonId == null ? metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement().getName() : null;
            String toExternalId = toEtalonId == null ? IdUtils.v1String() : null;
            String toEntityName = toEtalonId == null ? metaModelService.instance(Descriptors.DATA).getRelation(ro.getRelName()).getRight().getName() : null;
            Date validFrom = ro.getEtalonRecord() != null ? ConvertUtils.localDateTime2Date(ro.getEtalonRecord().getValidFrom()) : null;
            Date validTo = ro.getEtalonRecord() != null ? ConvertUtils.localDateTime2Date(ro.getEtalonRecord().getValidTo()) : null;
            OperationType operationType = ro.getEtalonRecord() != null && StringUtils.isNoneBlank(ro.getEtalonRecord().getOperationType())
                    ? OperationType.valueOf(ro.getEtalonRecord().getOperationType())
                    : null;

            UpsertRelationsRequestContext ctx = UpsertRelationsRequestContext.builder()
                    .etalonKey(etalonId)
                    .relationsFrom(Collections.singletonMap(ro.getRelName(), Collections.singletonList(
                            UpsertRelationRequestContext.builder()
                                    .relationEtalonKey(ro.getEtalonId())
                                    .etalonKey(toEtalonId)
                                    .sourceSystem(toSsourceSystem)
                                    .externalId(toExternalId)
                                    .entityName(toEntityName)
                                    .record(converted)
                                    .relationName(ro.getRelName())
                                    .validFrom(validFrom)
                                    .validTo(validTo)
                                    .build())))
                    .build();

            if (operationType == OperationType.COPY) {
//                ctx.putToStorage(StorageId.DATA_UPSERT_VISTORY_OPERATION_TYPE, operationType);// TODO @Modules
            }

            UpsertRelationsDTO result = null;
            //TODO: Same as in data entity service. Need to add normal error processing.

            try {
                result = dataRelationsService.upsertRelations(ctx);
            } catch (Exception exc) {
                // TODO @Modules
//                if (CollectionUtils.isNotEmpty(ctx.getDqErrors())) {
//                    DataRecordEtalonConverter.copyDQErrors(ctx.getDqErrors(), ro.getEtalonRecord().getDqErrors());
//                    return Response.ok(new RestResponse<>(ro, false)).build();
//                } else {
//                    throw exc;
//                }
                throw exc;
            }

            EtalonIntegralRecordRO updated = null;
            List<ErrorInfo> errors = null;

            // Single record i expected
            Iterator<Entry<RelationStateDTO, List<UpsertRelationDTO>>> i
                    = result != null && result.getRelations() != null
                    ? result.getRelations().entrySet().iterator()
                    : null;

            while (i != null && i.hasNext()) {
                List<UpsertRelationDTO> relations = i.next().getValue();
                UpsertRelationDTO dto = relations != null && !relations.isEmpty() ? relations.get(0) : null;
                EtalonRelation relation = dto != null ? dto.getEtalon() : null;

                if(dto != null && CollectionUtils.isNotEmpty(dto.getErrors())){
                    errors = dto.getErrors()
                            .stream()
                            .map(ErrorInfoToRestErrorInfoConverter::convert)
                            .collect(Collectors.toList());
                }


                updated = IntegralRecordEtalonConverter.to(relation);
                if (updated != null) {
                    updated.setEtalonId(dto == null ? null : dto.getRelationKeys().getEtalonKey().getId());
                    // TODO @Modules
//                    updated.getEtalonRecord().setWorkflowState(WorkflowTaskConverter.to(dto == null ? null : dto.getTasks()));
                    updated.getEtalonRecord().setRights(RoleRoConverter.convertResourceSpecificRights(dto == null ? null : dto.getRights()));
                }

                break;
            }

            RestResponse response = new RestResponse<>(updated);
            response.setErrors(errors);

            return Response.ok(response).build();
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * Inactivation of an etalon relation.
     * @param etalonId etalon id
     * @return response
     */
    @DELETE
    @Path("/" + PATH_PARAM_RELATION + "/{" + RestConstants.DATA_PARAM_ID + "}")
    @Operation(
        description = "Удалить список связей к текущему объекту.",
        method = HttpMethod.DELETE,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response deactivateEtalonRelation(
            @Parameter(description = "ID эталона", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId){

        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_RELATIONS_ETALON_DELETE);
        MeasurementPoint.start();
        try {

            DeleteRelationRequestContext ctx = DeleteRelationRequestContext.builder()
                    .relationEtalonKey(etalonId)
                    .inactivateEtalon(true)
                    .build();
            // TODO @Modules
//            ctx.putToStorage(StorageId.DELETE_BY_RELATION, etalonId);

            DeleteRelationDTO result = dataRelationsService.deleteRelation(ctx);
            RestResponse response = new RestResponse<>(result != null ? Boolean.TRUE : Boolean.FALSE);
            if(CollectionUtils.isNotEmpty(result.getErrors())){
                response.setErrors(result.getErrors()
                        .stream()
                        .map(ErrorInfoToRestErrorInfoConverter::convert)
                        .collect(Collectors.toList()));
            }

            return Response.ok(response).build();
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * Inactivation of an origin relation.
     * @param originId origin id
     * @return response
     */
    @DELETE
    @Path("/" + PATH_PARAM_RELATION + "/" + RestConstants.PATH_PARAM_ORIGIN + "/{" + RestConstants.DATA_PARAM_ID + "}")
    @Operation(
        description = "Удалить список связей к текущему объекту.",
        method = HttpMethod.DELETE,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response deactivateOriginRelation(
            @Parameter(description = "ID эталона", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String originId){

        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_RELATIONS_ORIGIN_DELETE);
        MeasurementPoint.start();
        try {

            DeleteRelationRequestContext ctx = DeleteRelationRequestContext.builder()
                    .relationOriginKey(originId)
                    .inactivateOrigin(true)
                    .build();

            DeleteRelationDTO result = dataRelationsService.deleteRelation(ctx);
            RestResponse response = new RestResponse<>(result != null ? Boolean.TRUE : Boolean.FALSE);
            if(CollectionUtils.isNotEmpty(result.getErrors())){
                response.setErrors(result.getErrors()
                        .stream()
                        .map(ErrorInfoToRestErrorInfoConverter::convert)
                        .collect(Collectors.toList()));
            }

            return Response.ok(response).build();
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * Inactivation of an origin relation.
     * @param versionId version id
     * @param timestamps the timestamps
     * @return response
     */
    @DELETE
    @Path("/" + PATH_PARAM_RELATION
            + "/" + RestConstants.PATH_PARAM_VERSION + "/{" + RestConstants.DATA_PARAM_ID + "}/{" + RestConstants.DATA_PARAM_TIMESTAMPS + ":.*}")
    @Operation(
        description = "Удалить список связей к текущему объекту.",
        method = HttpMethod.DELETE,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response deactivateVersionRelation(
            @Parameter(description = "ID эталона", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String versionId,
            @Parameter(description = "Значения границ интервалов. Всегда 2 элемента", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_TIMESTAMPS) List<PathSegment> timestamps){

        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_RELATIONS_VERSION_DELETE);
        MeasurementPoint.start();
        try {

            if (timestamps == null || timestamps.size() != 2) {
                throw new RuntimeException("Timestamps aren't even!");
            }

            Date validFrom = RestUtils.extractStart(timestamps);
            Date validTo = RestUtils.extractEnd(timestamps);

            DeleteRelationRequestContext ctx = DeleteRelationRequestContext.builder()
                    .relationEtalonKey(versionId)
                    .inactivatePeriod(true)
                    .validFrom(validFrom)
                    .validTo(validTo)
                    .build();

            DeleteRelationDTO result = dataRelationsService.deleteRelation(ctx);
            RestResponse response = new RestResponse<>(result != null ? Boolean.TRUE : Boolean.FALSE);
            if(CollectionUtils.isNotEmpty(result.getErrors())){
                response.setErrors(result.getErrors()
                        .stream()
                        .map(ErrorInfoToRestErrorInfoConverter::convert)
                        .collect(Collectors.toList()));
            }

            return Response.ok(response).build();
        } finally {
            MeasurementPoint.stop();
        }
    }


    private SearchResultRO getRelationsToSideByTimelineAndFromEtalonId(String etalonId,
                                                                       Date validFrom,
                                                                       Date validTo,
                                                                       RelationElement relationDef,
                                                                       List<String> returnFields) {

        FormField fromEtalon = FormField.exact(FIELD_FROM_ETALON_ID, etalonId);
        FormField notFrom = FormField.notInRange(RelationHeaderField.FIELD_TO, null, validFrom);
        FormField notTo = FormField.notInRange(FIELD_FROM, validTo, null);
        FormField relName = FormField.exact(FIELD_RELATION_NAME, relationDef.getName());
        FormField direct = FormField.exact(FIELD_DIRECTION_FROM, false);

        List<String> searchFields = new ArrayList<>();
        searchFields.add(RelationHeaderField.FIELD_CREATED_AT.getName());
        searchFields.add(RelationHeaderField.FIELD_UPDATED_AT.getName());
        searchFields.addAll(returnFields);

        SearchRequestContext mainContext = SearchRequestContext.builder(EntityIndexType.RECORD, relationDef.getRight().getName(), SecurityUtils.getCurrentUserStorageId())
                .totalCount(true)
                .fetchAll(true)
                .count(1000)
                .returnFields(searchFields)
                .build();

        SearchRequestContext relationsContext = SearchRequestContext.builder(EntityIndexType.RELATION, relationDef.getRight().getName(), SecurityUtils.getCurrentUserStorageId())
                .query(SearchQuery.formQuery(FieldsGroup.and(fromEtalon, notFrom, notTo, relName, direct)))
                .returnFields(Arrays.asList(FIELD_FROM.getName(), RelationHeaderField.FIELD_TO.getName()))
                .count(1000)
                .build();

        ComplexSearchRequestContext getToSide = ComplexSearchRequestContext.hierarchical(mainContext, relationsContext);
        ComplexSearchResultDTO toSide = searchService.search(getToSide);
        searchResultHitModifier.modifySearchResult(toSide.getMain());
        return SearchResultToRestSearchResultConverter.convert(toSide.getMain());

    }

    private SearchResultDTO getRelationsByTimelineAndFromEtalonId(String etalonId,
                                                                  Date validFrom,
                                                                  Date validTo,
                                                                  RelationElement relationDef,
                                                                  List<String> returnFields) {        // restriction for get relations data

        FormField fromEtalon = FormField.exact(FIELD_FROM_ETALON_ID, etalonId);
        FormField notFrom = FormField.notInRange(RelationHeaderField.FIELD_TO, null, validFrom);
        FormField notTo = FormField.notInRange(FIELD_FROM, validTo, null);
        FormField relName = FormField.exact(FIELD_RELATION_NAME, relationDef.getName());
        FormField direct = FormField.exact(FIELD_DIRECTION_FROM, false);

        List<String> searchFields = new ArrayList<>();
        searchFields.add(FIELD_FROM.getName());
        searchFields.add(RelationHeaderField.FIELD_ETALON_ID.getName());
        searchFields.add(RelationHeaderField.FIELD_TO.getName());
        searchFields.add(RelationHeaderField.FIELD_TO_ETALON_ID.getName());
        searchFields.add(RelationHeaderField.FIELD_CREATED_AT.getName());
        searchFields.add(RelationHeaderField.FIELD_UPDATED_AT.getName());
        if(CollectionUtils.isNotEmpty(returnFields)){
            searchFields.addAll(returnFields);
        }

        SearchRequestContext relationsContext = SearchRequestContext.builder(EntityIndexType.RELATION, relationDef.getRight().getName(), SecurityUtils.getCurrentUserStorageId())
                .query(SearchQuery.formQuery(FieldsGroup.and(fromEtalon, notFrom, notTo, relName, direct)))
                .count(1000)
                .scrollScan(true)
                .returnFields(searchFields)
                .build();

        SearchResultDTO result = searchService.search(relationsContext);
        if (CollectionUtils.isNotEmpty(result.getHits())) {
            searchResultHitModifier.modifySearchResult(result,
                    relationDef.getName(),
                    "",
                    EnumSet.allOf(SearchResultProcessingElements.class));
        }

        return result;
    }


}
